﻿//******************************************************************************************************
//  AlarmAdapter.cs - Gbtc
//
//  Copyright © 2010, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  01/31/2012 - Stephen C. Wills
//       Generated original version of source code.
//  12/26/2012 - Prasanna Kumari
//       Applied changes for Migrating adapters from the Time Series Framework to SIEGate Core
//  2/2013 - University of Illinois, Information Trust Institute, Coordinated Science Laboratory
//       Updated code to work for the SIEGate system and architecture
//
//
//******************************************************************************************************

using DataQualityMonitoring.Services;
using GSF;
using GSF.Collections;
using GSF.Data;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using SIEGate.Core;
using SIEGate.Core.Adapters;
using SIEGate.Core.Legacy;
using SIEGate.Core.Configurator;
using SIEGate.Core.Measurements;
using SIEGate.Core.Measurements.Values;

namespace DataQualityMonitoring
{
    /// <summary>
    /// Action adapter that generates alarm measurements based on alarm definitions from the database.
    /// </summary>
    [Description("Alarm: Generates alarm events for alarms defined in the database.")]
    public class AlarmAdapter : ActionAdapter, IDisposable
    {
        #region [ Members ]

        // Nested Types
        public class AdapterInfo : AdapterInfoBase
        {
            #region [ Members ]

            // Fields
            private List<Alarm> m_alarms;
            private Dictionary<Guid, List<Alarm>> m_alarmLookup;

            #endregion

            #region [ Properties ]

            internal List<Alarm> Alarms
            {
                get
                {
                    return m_alarms;
                }
            }

            internal Dictionary<Guid, List<Alarm>> AlarmLookup
            {
                get
                {
                    return m_alarmLookup;
                }
            }

            #endregion

            #region [ Methods ]

            public override void ExtractConfiguration(DataSet configurationTables)
            {
                string filterExpression;

                base.ExtractConfiguration(configurationTables);

                // Create alarms using definitions from the database
                m_alarms = configurationTables.Tables["Alarms"].Rows.Cast<DataRow>()
                    .Where(row => row.ConvertField<bool>("Enabled"))
                    .Select(CreateAlarm)
                    .ToList();

                // Create lookup table for alarms to speed up measurement processing
                m_alarmLookup = new Dictionary<Guid, List<Alarm>>();

                foreach (Guid signalID in Alarms.Select(alarm => alarm.SignalID).Distinct())
                    AlarmLookup.Add(signalID, Alarms.Where(alarm => alarm.SignalID == signalID).ToList());

                if (Alarms.Count > 0)
                {
                    // Generate filter expression for input measurements
                    filterExpression = Alarms.Select(a => a.SignalID)
                        .Distinct()
                        .Select(id => id.ToString())
                        .Aggregate((list, id) => list + ";" + id);

                    // Set input measurement keys for measurement routing
                    IncomingStreamIDs = GsfHandler.ParseStreamIDs(configurationTables, filterExpression);
                }
            }

            // Creates an alarm using data defined in the database.
            private Alarm CreateAlarm(DataRow row)
            {
                object associatedMeasurementId = row.Field<object>("AssociatedMeasurementID");

                return new Alarm()
                {
                    ID = row.ConvertField<int>("ID"),
                    TagName = row.Field<object>("TagName").ToString(),
                    SignalID = Guid.Parse(row.Field<object>("SignalID").ToString()),
                    AssociatedMeasurementID = ((object)associatedMeasurementId != null) ? Guid.Parse(associatedMeasurementId.ToString()) : (Guid?)null,
                    Description = row.Field<object>("Description").ToNonNullString(),
                    Severity = (AlarmSeverity)row.ConvertField<int>("Severity"),
                    Operation = (AlarmOperation)row.ConvertField<int>("Operation"),
                    SetPoint = row.ConvertNullableField<double>("SetPoint"),
                    Tolerance = row.ConvertNullableField<double>("Tolerance"),
                    Delay = row.ConvertNullableField<double>("Delay"),
                    Hysteresis = row.ConvertNullableField<double>("Hysteresis")
                };
            }

            #endregion
        }

        // Fields
        private AlarmService m_alarmService;
        private long m_eventCount;
        private long m_processedMeasurements;
        private bool m_disposed;

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Creates a new instance of the <see cref="AlarmAdapter"/>.
        /// </summary>
        public AlarmAdapter(AdapterInfo conf)
            : base(conf)
        {
            Initialize();
        }

        /// <summary>
        /// Releases the unmanaged resources before the <see cref="AlarmAdapter"/> object is reclaimed by <see cref="GC"/>.
        /// </summary>
        ~AlarmAdapter()
        {
            Dispose(false);
        }

        #endregion

        #region [ Properties ]

        public new AdapterInfo Info
        {
            get
            {
                return (AdapterInfo)base.Info;
            }
            set
            {
                base.Info = value;
            }
        }

        /// <summary>
        /// Returns the detailed status of the data input source.
        /// </summary>
        public string Status
        {
            get
            {
                StringBuilder statusBuilder = new StringBuilder();
                statusBuilder.Append(m_alarmService.Status);
                return statusBuilder.ToString();
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Initializes the <see cref="AlarmAdapter"/>.
        /// </summary>
        public void Initialize()
        {
            // Set up alarm service
            m_alarmService = new AlarmService(this);
            m_alarmService.SettingsCategory = Info.Name.Replace("!", "").ToLower() + m_alarmService.SettingsCategory;
            m_alarmService.ServiceProcessException += AlarmService_ServiceProcessException;
            m_alarmService.PersistSettings = true;
            m_alarmService.Initialize();
        }

        /// <summary>
        /// Releases all the resources used by the <see cref="AlarmAdapter"/> object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="AlarmAdapter"/> object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                try
                {
                    if (disposing)
                    {
                        m_alarmService.ServiceProcessException -= AlarmService_ServiceProcessException;
                        m_alarmService.Dispose();
                    }
                }
                finally
                {
                    m_disposed = true;  // Prevent duplicate dispose.
                }
            }
        }

        protected override void HandleMeasurement(IMeasurement m)
        {
            List<Alarm> alarms;
            List<Alarm> raisedAlarms;
            List<IMeasurement> alarmEvents;

            // Create the collection that will store
            // alarm events to be sent into the system
            alarmEvents = new List<IMeasurement>();

            lock (Info.Alarms)
            {
                // Get alarms that apply to the measurement being processed
                if (!Info.AlarmLookup.TryGetValue(m.StreamID, out alarms))
                    alarms = new List<Alarm>();

                // Get alarms that triggered events
                raisedAlarms = alarms.Where(a => a.Test(m)).ToList();
            }

            // Create event measurements and send them into the system
            foreach (Alarm alarm in raisedAlarms)
            {
                Guid streamID;
                Ticks timestamp;
                MeasurementStateFlags flags;
                IMeasurementValue value;

                if ((object)alarm.AssociatedMeasurementID != null)
                    streamID = (Guid)alarm.AssociatedMeasurementID;
                else
                    streamID = Guid.Empty;

                timestamp = m.Timestamp;
                flags = MeasurementStateFlags.Normal;
                value = new IntegerValue((int)alarm.State);
                alarmEvents.Add(new Measurement(streamID, timestamp, flags, value));
                m_eventCount++;
            }

            // Send new alarm events into the system,
            // then reset the collection for the next
            // group of measurements
            foreach (IMeasurement alarmEvent in alarmEvents)
                OnEmit(alarmEvent);

            alarmEvents.Clear();

            // Increment total count of processed measurements
            m_processedMeasurements++;
        }

        /// <summary>
        /// Gets a short one-line status of this <see cref="LegacyAdapterBase"/>.
        /// </summary>
        /// <param name="maxLength">Maximum number of available characters for display.</param>
        /// <returns>A short one-line summary of the current status of this <see cref="LegacyAdapterBase"/>.</returns>
        public string GetShortStatus(int maxLength)
        {
            return string.Format("{0} events processed since last start", m_eventCount).CenterText(maxLength);
        }

        /// <summary>
        /// Gets a collection containing all the raised alarms in the system.
        /// </summary>
        /// <returns>A collection containing all the raised alarms.</returns>
        public ICollection<Alarm> GetRaisedAlarms()
        {
            lock (Info.Alarms)
            {
                return Info.Alarms.Where(alarm => alarm.State == AlarmState.Raised)
                    .ToList();
            }
        }

        // Processes excpetions thrown by the alarm service.
        private void AlarmService_ServiceProcessException(object sender, EventArgs<Exception> e)
        {
            Exception ex = e.Argument;
            LogDispatcher.E("AlarmService", "error", "{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace);
        }

        #endregion
    }
}
